package ffmpeg

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"github.com/kaylee595/tracerr"
	"github.com/spf13/cast"
	"image"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

type Client struct {
	binFilename string
	loglevel    Loglevel
}

type Option func(*Client)

// WithLoglevel 设置输出的日志级别
func WithLoglevel(loglevel Loglevel) Option {
	return func(e *Client) {
		e.loglevel = loglevel
	}
}

func NewClient(binFilename string, options ...Option) (*Client, error) {
	abs, _ := filepath.Abs(binFilename)
	_, err := os.Lstat(abs)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, tracerr.Wrap(ErrFfmpegFileNotExist)
		}
		return nil, tracerr.Wrap(err)
	}
	e := &Client{
		binFilename: abs,
		loglevel:    LogLevelInfo,
	}
	for _, option := range options {
		option(e)
	}
	return e, nil
}

func (c *Client) ExecCtx(ctx context.Context, args ...string) error {
	_, err := c.ExecCtxValue(ctx, args...)
	return tracerr.Wrap(err)
}

func (c *Client) ExecCtxValue(ctx context.Context, args ...string) (string, error) {
	args = append(args, "-loglevel", cast.ToString(int(c.loglevel)))
	echo := new(bytes.Buffer)

	cmd := exec.CommandContext(ctx, c.binFilename, args...)
	cmd.Stderr = echo
	cmd.Stdout = echo
	err := cmd.Run()
	echoString := echo.String()
	echoString = strings.TrimRight(echoString, "\r\n\t ")
	if err != nil {
		switch {
		case strings.HasSuffix(echoString, "At least one output file must be specified"):
			err = tracerr.Wrap(ErrOutputFileMustBeSpecified)
		case strings.HasSuffix(echoString, "Invalid argument"):
			err = tracerr.Wrap(ErrInvalidArgument)
		default:
			err = tracerr.Wrap(err)
		}
		return echoString, tracerr.Wrap(&ExecError{
			Err:  err,
			Echo: echoString,
		})
	}
	return echoString, nil
}

// VideoFrameExtraction 将视频以每秒24帧的方式提取
// videoFile 视频文件
// outputDir 输出目录
func (c *Client) VideoFrameExtraction(ctx context.Context, videoFile, outputDir string) error {
	args := []string{
		"-i", videoFile,
		"-vf", "fps=24",
		"-f", "image2",
		"-y",
		filepath.Join(outputDir, "%013d.png"),
	}
	return tracerr.Wrap(c.ExecCtx(ctx, args...))
}

// MergeImg2Video 合并图片成视频
func (c *Client) MergeImg2Video(ctx context.Context, imgDir, audioFile, outputFile string) error {
	args := []string{
		"-y",
		"-r", "24",
		"-i", filepath.Join(imgDir, "%013d.png"),
		"-i", audioFile,
		"-vcodec", "libx264",
		"-c:a", "aac",
		"-crf", "20",
		"-preset", "slow",
		"-movflags", "faststart",
		"-vf", "format=yuv420p",
		outputFile,
	}
	return tracerr.Wrap(c.ExecCtx(ctx, args...))
}

// Overlay 视频叠加
func (c *Client) Overlay(ctx context.Context, isVideo bool, outputFile string, elementFile ...string) error {
	var args []string
	var filterComplexOverlay string
	for index, item := range elementFile {
		args = append(args, "-i", item)
		filterComplexOverlay += fmt.Sprintf("[%d:v]", index)
	}
	args = append(args,
		"-filter_complex", filterComplexOverlay+"overlay",
		"-crf", "18",
	)
	if isVideo {
		args = append(args,
			"-codec:a", "copy",
			"-c:v", "libx264",
		)
	}
	args = append(args, "-y", outputFile)
	return tracerr.Wrap(c.ExecCtx(ctx, args...))
}

// GenerateVideoPreviewImg 视频预览图
func (c *Client) GenerateVideoPreviewImg(ctx context.Context, videoFilePath, outputFilePath string) error {
	args := []string{
		"-i", videoFilePath,
		"-vf", "select=eq(n\\,0)",
		"-vframes", "1",
		outputFilePath,
	}
	return tracerr.Wrap(c.ExecCtx(ctx, args...))
}

// Scale 缩放
func (c *Client) Scale(ctx context.Context, input string, w, h int, output string) error {
	args := []string{
		"-i", input,
		"-vf", fmt.Sprintf("scale=%d:%d", w, h),
		output,
	}
	return tracerr.Wrap(c.ExecCtx(ctx, args...))
}

type MediaInfo struct {
	Duration time.Duration
	Video    struct {
		Encoder    string
		ColorSpace string
		Resolution image.Point
		Bitrate    string
		FPS        string
	}
	Audio struct {
		Encoder string
		HZ      int
	}
}

// GetMediaInfo 获取视频信息
func (c *Client) GetMediaInfo(ctx context.Context, input string) (*MediaInfo, error) {
	info, err := c.ExecCtxValue(ctx, "-i", input)
	if err != nil && !errors.Is(err, ErrOutputFileMustBeSpecified) {
		return nil, err
	}
	ret := new(MediaInfo)
	var isScan bool
	for _, line := range strings.Split(info, "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "Input #0") {
			isScan = true
			continue
		}
		if !isScan {
			continue
		}
		switch {
		case strings.HasPrefix(line, "Duration: "):
			cols := strings.Split(line, ", ")
			for _, col := range cols {
				k, v, ok := strings.Cut(col, ": ")
				if !ok {
					continue
				}
				switch k {
				case "Duration":
					// 02:56:20.04
					// 00:00:12.80
					durationParts := strings.Split(v, ":")
					if len(durationParts) < 3 {
						break
					}
					v = strings.Replace(v, ":", "h", 1)
					v = strings.Replace(v, ":", "m", 1)
					v += "s"
					duration, err := time.ParseDuration(v)
					if err != nil {
						break
					}
					ret.Duration = duration
				}
			}
		case strings.HasPrefix(line, "Stream"):
			//  #0:0(und): Video:
			_, v, ok := strings.Cut(line, ": ")
			if !ok {
				break
			}
			k, v, ok := strings.Cut(v, ": ")
			if !ok {
				break
			}
			switch k {
			case "Video":
				// 将括号中的", "换成"%,% "
				tmp := v
				v = ""
				for {
					// h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709), 1080x1920 [SAR 1:1 DAR 9:16], 11486 kb/s, 30 fps, 30 tbr, 30 tbn, 60 tbc (default)
					index := strings.Index(tmp, ", ")
					if index == -1 {
						break
					}
					var inBrackets bool
					leftIndex := strings.Index(tmp[:index], "(")
					rightIndex := strings.Index(tmp[:index], ")")
					inBrackets = leftIndex > rightIndex
					if inBrackets {
						v += tmp[:index] + "%,% "
					} else {
						v += tmp[:index+2]
					}
					tmp = tmp[index+2:]
				}
				cols := strings.Split(v, ", ")
				if len(cols) < 5 {
					break
				}
				for i := range cols {
					cols[i] = strings.ReplaceAll(cols[i], "%,% ", ", ")
				}
				ret.Video.Encoder = cols[0]
				ret.Video.ColorSpace = cols[1]
				resolution, _, _ := strings.Cut(cols[2], " ")
				x, y, found := strings.Cut(resolution, "x")
				if found {
					ret.Video.Resolution = image.Point{
						X: cast.ToInt(x),
						Y: cast.ToInt(y),
					}
				}
				ret.Video.Bitrate = cols[3]
				ret.Video.FPS = cols[4]
			case "Audio":
				cols := strings.Split(v, ", ")
				if len(cols) < 2 {
					break
				}
				ret.Audio.Encoder = cols[0]
				hz, _, _ := strings.Cut(cols[1], " ")
				ret.Audio.HZ = cast.ToInt(hz)
			}
		}
	}
	return ret, nil
}

// FormatConversion 格式转换
func (c *Client) FormatConversion(ctx context.Context, mediaFilename, outFilename string) error {
	// ffmpeg -i "%%a" -c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc "mp4\%%~na.mp4"
	args := []string{
		"-i", mediaFilename,
		"-acodec", "copy",
		"-vcodec", "copy",
		"-absf", "aac_adtstoasc",
		"-movflags", "faststart",
		"-y", outFilename,
	}
	return tracerr.Wrap(c.ExecCtx(ctx, args...))
}
